﻿using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Data.Linq;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Text;
using System.Threading;

using Ordinaire;
using Ordinaire.Db;
using Ordinaire.Commerce.Db;

namespace Ordinaire.Commerce.Db
{
    /// <summary>
    /// Helps create a database on the server with other database components from other extensions.
    /// </summary>
    public class DatabaseBuilder
    {
        #region Variables

        /// <summary>
        /// Default assembly name
        /// </summary>
        private const string DefaultAssemblyName = "Ordinaire.Commerce.DynamicAssembly";

        /// <summary>
        /// Default type name
        /// </summary>
        private const string DefaultTypeName = "DynamicContext";

        /// <summary>
        /// Instance of the dynamic type created.
        /// </summary>
        private DataContext context = null;

        /// <summary>
        /// Dynamic type derived from Ordinaire.Commerce.Db.CContext.
        /// </summary>
        private Type type = null;

        /// <summary>
        /// Instance of System.Reflection.Emit.TypeBuilder for reuse purpose across different functions.
        /// </summary>
        private TypeBuilder typeBuilder;

        /// <summary>
        /// The other database components to build into.
        /// </summary>
        [ImportMany]
        public IEnumerable<Lazy<IDatabasePart>> databaseParts { get; set; }

        #endregion

        #region Constructor

        /// <summary>
        /// Returns new instance of Ordinaire.Commerce.DatabaseBuilder.
        /// </summary>
        public DatabaseBuilder()
        {
            AssemblyName    assemblyName    = new AssemblyName(DefaultAssemblyName);
            AssemblyBuilder assemblyBuilder = Thread.GetDomain().DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
            ModuleBuilder   moduleBuilder   = assemblyBuilder.DefineDynamicModule(assemblyName.Name);

            // Create new type derived from Ordinaire.Commerce.Db.CContext
            Type ccontextType = typeof(CContext);
            this.typeBuilder  = moduleBuilder.DefineType(DefaultTypeName, TypeAttributes.Public, ccontextType);

            // Create the new type constructor
            Type[]          constructorArgs     = { typeof(string) };
            ConstructorInfo ccontextConstructor = ccontextType.GetConstructor(constructorArgs);

            ConstructorBuilder constructorBuilder = typeBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, constructorArgs);
            ILGenerator        constructorIL      = constructorBuilder.GetILGenerator();

            constructorIL.Emit(OpCodes.Ldarg_0);
            // Comment out below to use a defined connection string
            //myConstructorIL.Emit(OpCodes.Ldstr, @"Data Source=jloe-PC\SQLEXPRESS;Initial Catalog=testpos;Integrated Security=SSPI;");
            constructorIL.Emit(OpCodes.Ldarg_1);
            constructorIL.Emit(OpCodes.Call, ccontextConstructor);
            constructorIL.Emit(OpCodes.Ret);

            ExtensionContainer.Instance.ComposeParts(this);
        }

        #endregion

        #region Public methods

        /// <summary>
        /// Creates a database on the server.
        /// </summary>
        public void CreateDatabase()
        {
            this.context.ThrowIfNull<InvalidOperationException>("Try executing Load() first");

            MethodInfo methodInfo = this.type.GetMethod("CreateDatabase", Type.EmptyTypes);
            methodInfo.Invoke(this.context, null);
        }

        /// <summary>
        /// Determines whether the associated database can be opened.
        /// </summary>
        /// <returns></returns>
        public bool DatabaseExists()
        {
            this.context.ThrowIfNull<InvalidOperationException>("Try executing Load() first");

            MethodInfo methodInfo = this.type.GetMethod("DatabaseExists", Type.EmptyTypes);
            return (bool) methodInfo.Invoke(this.context, null);
        }

        /// <summary>
        /// Deletes the associated database.
        /// </summary>
        public void DeleteDatabase()
        {
            this.context.ThrowIfNull<InvalidOperationException>("Try executing Load() first");

            MethodInfo methodInfo = this.type.GetMethod("DeleteDatabase", Type.EmptyTypes);
            methodInfo.Invoke(this.context, null);
        }

        /// <summary>
        /// Collates database components from other extensions in order to create a complete database.
        /// </summary>
        public void Load()
        {
            // Collates database components from other extensions
            foreach (Lazy<IDatabasePart> dbPart in this.databaseParts)
            {
                FieldInfo[] fieldInfos = dbPart.Value.GetTables();
                for (int i = 0; i < fieldInfos.Length; i++)
                {
                    FieldInfo fieldInfo = fieldInfos[i];
                    this.typeBuilder.DefineField(fieldInfo.Name, fieldInfo.FieldType, FieldAttributes.Public);
                }
            }
            this.type    = this.typeBuilder.CreateType();
            this.context = (DataContext) (object) DbFactory.Instance.CreateDataContext(type);
        }

        #endregion
    }
}
